
from .observer import Observer, MemorizedObserver
from .memory import ReplayMemory, Transition
from .network import DQN, Network
import random
import math
import torch
import torch.nn as nn
import torch.optim as optim
from itertools import count


class Agent:
    def get_action(self, env, state, reward):
        return env.action_space.sample()

    def play(self, env, init_reward=.0, verbose=True):
        state = env.reset()
        reward = init_reward
        self.total_rewards = reward
        for step in count():
            action = self.get_action(env, state, reward)
            next_state, reward, done, info = env.step(action)
            self.total_rewards += reward
            if done:
                return self.done(step, next_state, reward, info, verbose)
            env.render()
            if verbose:
                args = (step, action, reward, self.total_rewards)
                print('{}: action={}, reward={}, total={}'.format(*args))
        env.close()

    def done(self, step, state, reward, info, verbose=True):
        if verbose:
            message = 'stop in {} steps with total rewards {}'
            print(message.format(step, self.total_rewards))
        return self.total_rewards


class Policy:
    def __init__(self, observers=None):
        if observers is None:
            self.observers = []
        elif isinstance(observers, Observer):
            self.observers = (observers,)
        else:
            self.observers = observers

    def reset(self, environment):
        self.env = environment
        state = environment.reset()
        for observer in self.observers:
            observer.on_reset(state)
        return state

    def select_action(self, state, env):
        raise NotImplementedError

    def observeration(self, state):
        return state

    def do_action(self, action, env, state):
        next_state, reward, done, info = env.step(action.item() if isinstance(action, torch.Tensor) else action)
        next_state = self.observeration(next_state)
        reward = self.observeration(reward)
        for observer in self.observers:
            observer.on_action(state, action, next_state, reward, done, info)
        return next_state, reward, done, info

    def run_episode(self, env, train=False):
        from itertools import count
        state = self.reset(env)
        for t in count():
            action = self.select_action(state, env)
            next_state, reward, done, info = self.do_action(action, env, state)
            if done:
                return self.done(t + 1)
            else:
                state = next_state
                if train:
                    self.optimize()
        raise Exception()

    def done(self, steps):
        for observer in self.observers:
            observer.on_done(steps)
        return steps

    def run(self, env, num_episodes=50):
        for i_episode in range(num_episodes):
            steps = self.run_episode(env)
            print(f'run episode {i_episode}: done after {steps} steps.')

class MemorizedPolicy(Policy):
    def __init__(self, observers=None):
        super(MemorizedPolicy, self).__init__(observers=observers)
        if self.memory is None:
            self.observers.append(MemorizedObserver(10000))

    @property
    def memory(self):
        if self.observers is None:
            return None
        if hasattr(self, '_memory'):
            return self._memory
        for observer in self.observers:
            if isinstance(observer, MemorizedObserver):
                self._memory = observer.memory
                return self._memory
        return None


class RandomPolicy(Policy):
    def select_action(self, state, env):
        return env.action_space.sample()

class NetworkPolicy(MemorizedPolicy):
    def __init__(self, observers=None, device='cpu'):
        super(NetworkPolicy, self).__init__(observers)
        self.device = device

    def reset(self, env):
        self.steps_done = 0
        state = super(NetworkPolicy, self).reset(env)
        n_actions = env.action_space.n
        self.network = Network(state.shape, n_actions).to(self.device)
        return self.observeration(state)

    def observeration(self, state):
        return torch.tensor(state, device=self.device, dtype=torch.float)

    def select_action(self, state, env, EPS_START=0.9, EPS_END=0.05, EPS_DECAY=200):
        sample = random.random()
        eps_threshold = EPS_END + (EPS_START - EPS_END) * \
            math.exp(-1. * self.steps_done / EPS_DECAY)
        self.steps_done += 1
        if sample > eps_threshold:
            with torch.no_grad():
                action =  self.network(state).max(0)[1].view(1, 1)
        else:
            n_actions = env.action_space.n
            action = torch.tensor([[random.randrange(n_actions)]], device=self.device, dtype=torch.long)
        return action

    def optimize(self, env, BATCH_SIZE=128, GAMMA=0.999):
        if self.memory is None:
            raise Exception('memory not found.')
        if len(self.memory) < BATCH_SIZE:
            return
        transitions = self.memory.sample(BATCH_SIZE)
        print(f'transitions = {transitions}')
        batch = Transition(*zip(*transitions))
        print(f'batch = {batch}')
        is_next_state_none = tuple(map(lambda s: s is not None, batch.next_state))
        non_final_mask = torch.tensor(is_next_state_none, device=self.device, dtype=torch.bool)
        non_final_next_states = torch.cat([s for s in batch.next_state
                                                    if s is not None])
        state_batch = torch.cat(batch.state)
        action_batch = torch.cat(batch.action)
        reward_batch = torch.cat(batch.reward)

        # Compute Q(s_t, a) - the model computes Q(s_t), then we select the
        # columns of actions taken. These are the actions which would've been taken
        # for each batch state according to policy_net
        state_action_values = self.policy_net(state_batch).gather(1, action_batch)

        # Compute V(s_{t+1}) for all next states.
        # Expected values of actions for non_final_next_states are computed based
        # on the "older" target_net; selecting their best reward with max(1)[0].
        # This is merged based on the mask, such that we'll have either the expected
        # state value or 0 in case the state was final.
        next_state_values = torch.zeros(BATCH_SIZE, device=device)
        next_state_values[non_final_mask] = self.target_net(non_final_next_states).max(1)[0].detach()
        # Compute the expected Q values
        expected_state_action_values = (next_state_values * GAMMA) + reward_batch
        # Compute Huber loss
        criterion = nn.SmoothL1Loss()
        loss = criterion(state_action_values, expected_state_action_values.unsqueeze(1))
        # Optimize the model
        self.optimizer.zero_grad()
        loss.backward()
        for param in self.policy_net.parameters():
            param.grad.data.clamp_(-1, 1)
        self.optimizer.step()


class QLearningPolicy(Policy):
    def __init__(self, observers=None):
        self.steps_done = 0
        self.device = 'cpu'
        super(QLearningPolicy, self).__init__(observers=observers)
        if self.memory is None:
            self.observers.append(MemorizedObserver(10000))

    def reset(self, env):
        state = env.reset()
        print(f'state: {state}')
        screen_height, screen_width = state.shape
        # init_screen = env.render()
        # _, _, screen_height, screen_width = init_screen.shape
        # n_actions = env.action_space.n
        self.policy_net = DQN(screen_height, screen_width, n_actions).to(self.device)
        self.target_net = DQN(screen_height, screen_width, n_actions).to(self.device)
        self.target_net.load_state_dict(self.policy_net.state_dict())
        self.target_net.eval()
        self.optimizer = optim.RMSprop(self.policy_net.parameters())
        return state


    def plot_durations(self, episode_durations):
        import matplotlib
        import matplotlib.pyplot as plt
        plt.clf()
        durations_t = torch.tensor(episode_durations, dtype=torch.float)
        plt.title('Training...')
        plt.xlabel('Episode')
        plt.ylabel('Duration')
        plt.plot(durations_t.numpy())
        # Take 100 episode averages and plot them too
        if len(durations_t) >= 100:
            means = durations_t.unfold(0, 100, 1).mean(1).view(-1)
            means = torch.cat((torch.zeros(99), means))
            plt.plot(means.numpy())
        # plt.pause(0.001)  # pause a bit so that plots are updated
        is_ipython = 'inline' in matplotlib.get_backend()
        if is_ipython:
            from IPython import display
            display.clear_output(wait=True)
            display.display(plt.gcf())
        else:
            plt.show()


    # def run(self, env, num_episodes=50, TARGET_UPDATE=10):
    #     episode_durations = 0
    #     self.reset(env)
    #     for i_episode in range(num_episodes):
    #         steps_done = self.run_episode(env)
    #         episode_durations.append(steps_done)
    #         # Update the target network, copying all weights and biases in DQN
    #         if i_episode % TARGET_UPDATE == 0:
    #             self.target_net.load_state_dict(self.policy_net.state_dict())
    #             self.plot_durations(episode_durations)
    #     print('Complete')
    #     env.render()
    #     env.close()
    #     return episode_durations
